<?php

/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */

/**
 * This file contains the PDORM_Mapper class.
 *
 * PHP version 5
 *
 * This file is part of PEAR::PDORM - A database object relational management
 * layer for PHP 5 built using PDO. PHP version 5.1+ is recommended.
 *
 * PEAR::PDORM is free software: you can redistribute it and/or modify it under
 * the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option)
 * any later version.
 *
 * PEAR::PDORM is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
 * details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with PEAR::PDORM. If not, see <http://www.gnu.org/licenses/>.
 *
 * @package     PDORM
 * @category    Object-relational Management
 * @author      Michael J. I. Jackson <mjijackson@gmail.com>
 * @copyright   2007 Michael J. I. Jackson
 * @license     http://www.gnu.org/licenses/lgpl-3.0.txt GNU LGPL 3.0
 * @version     SVN: $Id: Mapper.php 42 2007-12-12 04:59:11Z mjijackson $
 * @link        http://pear.php.net/packages/PDORM
 */

/**
 * Require PDORM_Domain
 */
require_once dirname(__FILE__) . DIRECTORY_SEPARATOR . 'Domain.php';

/**
 * The base class for all domain mappers. Domain mappers are singleton objects
 * that represent a single domain class and map domain object data to a
 * relational database.
 *
 * @package     PDORM
 * @category    Object-relational Management
 * @author      Michael J. I. Jackson <mjijackson@gmail.com>
 */
abstract class PDORM_Mapper
{

    /**#@+
     * @var     string
     * @access  protected
     */
    /**
     * This mapper's database table name. If this value is not explicitly
     * defined, it will be guessed from this mapper's object class name.
     */
    protected $table_name;

    /**
     * This mapper's database sequence name. If this value is not explicitly
     * defined, it will be constructed using this mapper's table name and this
     * mapper's connection's seqname_format connection option. See
     * {@link PDODB_Connection::__construct()} for more information on setting
     * connection options.
     */
    protected $sequence_name;

    /**
     * The name of this mapper's connection alias. The connection alias is a
     * string that can be used to quickly retrieve this mapper's database
     * connection instance using {@link PDORM::getConnection()}. If none
     * is specified, the 'default' alias will be used.
     */
    protected $connection_alias = 'default';

    /**
     * The name of the primary key of this mapper in this mapper's database
     * table. Defaults to 'id'.
     */
    protected $primary_key = 'id';

    /**
     * The name of the column in this mapper's database table that defines
     * inheritance relationships. Defaults to 'type'.
     */
    protected $inheritance_col;

    /**
     * The name of the domain class this mapper is designed to work with. If
     * left null, this value will be automatically derived from the mapper class
     * name.
     */
    protected $domain_cls;
    /**#@-*/

    /**#@+
     * Should be overridden in child classes.
     *
     * @var     array
     * @access  protected
     */
    /**
     * The names of domain classes this mapper's domain class contains one of.
     */
    protected $has_one = array();

    /**
     * The names of domain classes this mapper's domain class contains many of.
     */
    protected $has_many = array();

    /**
     * The names of domain classes this mapper's domain class belongs to.
     */
    protected $belongs_to = array();

    /**
     * The names of domain classes this mapper's domain class has a many-to-many
     * relationship with.
     */
    protected $many_to_many = array();
    /**#@-*/

    /**#@+
     * Should not be overridden in child classes.
     *
     * @var     array
     * @access  protected
     */
    /**
     * Contains the names of the fields for this mapper's domain class.
     */
    protected $field_names;

    /**
     * Contains the names of all the domain object classes that this mapper's
     * domain class belongs to or has one of.
     */
    protected $object_names;

    /**
     * Contains the names of all domain object classes of which this mapper's
     * domain object contains many of.
     */
    protected $collection_names;
    /**#@-*/

    /**
     * Contains all valid find options that may be used in the definition
     * of this mapper's domain class' relations with another.
     *
     * @var     array
     * @access  private
     * @static
     */
    private static $valid_find_keys = array(
        'select',
        'distinct',
        'join',
        'where',
        'group_by',
        'having',
        'order_by',
        'offset',
        'limit'
    );

    /**
     * Constructor. Sets up this mapper's domain class' relations with other
     * classes in the domain.
     *
     * @access  private
     */
    private function __construct()
    {
        $domain = PDORM_Domain::getInstance();
        $domain->setupRelations($this);
    }

    /**
     * This function is necessary because of the nature of static in PHP5. It
     * should return an instance of the mapper class. Note: This will not be
     * necessary in PHP6 when this class will rely on late
     * {@link http://php.net/~derick/meeting-notes.html#late-static-binding-using-this-without-or-perhaps-with-a-different-name run-time evaluation of static}.
     *
     * @access  public
     * @abstract
     * @static
     */
    abstract public static function getInstance();

    /**
     * Used by the magic {@link __call()} method to convert a camel cased field
     * name to the corresponding database field name. The $field_name parameter
     * will be the method name that was called without the findBy prefix. For
     * example, when the following method is called:
     *
     * <code>
     * $mapper->findByFirstName('joe'); // $field_name will be 'FirstName'
     * </code>
     *
     * the $field_name parameter will be 'FirstName'. This method is then
     * responsible for converting this string to the corresponding field name in
     * this mapper's database table. The default behavior of this function is
     * to convert the string to lowercase and place underscores before letters
     * that were capitalized using {@link PDORM_camel2uscore()}. In this example,
     * the string 'FirstName' would be converted to 'first_name'. This function
     * should be overridden in child mappers if a different behavior is desired.
     * PDORM provides several string conversion functions that should help to
     * achieve the desired result, including {@link PDORM_lcfirst()},
     * {@link PDORM_camel2uscore()}, and {@link PDORM_uscore2camel()}.
     *
     * @param   string  $field_name     The field name to convert
     * @return  string                  The database field name to use
     * @uses    $field_mode             Used to determine the conversion method
     * @access  public
     */
    public function fieldName($field_name)
    {
        return PDORM_camel2uscore($field_name);
    }

    /**
     * Generates a new id for a new object of this type. Defaults to returning
     * the next id in a database sequence with the same name as this mapper's
     * table name. Should be overridden in child classes if a different behavior
     * is desired.
     *
     * @return  int         The next id in this mapper's sequence
     * @access  public
     */
    public function newId()
    {
        return $this->conn()->nextId($this->sequenceName());
    }

    /**
     * Gets an array of {@link $valid_find_keys valid keys} that may be used in
     * find options between domain classes.
     *
     * @return  array           An array of valid find keys
     * @access  public
     * @static
     */
    public static function validFindKeys()
    {
        return self::$valid_find_keys;
    }

    /**
     * Supports findBy* methods where * represents one of this mapper's field
     * names. If you're having trouble getting this behavior working properly,
     * make sure that the string returned by {@link fieldName()} for a given
     * field name is contained in the array returned by {@link fieldNames()}. If
     * this mapper's connection is using something other than PDO::ATTR_CASE =
     * PDO::CASE_NATURAL, {@link fieldNames()} could be returning all
     * upper/lower case field names, thereby causing a mismatch.
     *
     * @param   string  $method     The method that was called
     * @param   array   $args       The arguments that were passed
     * @return  mixed               The result of the find method
     * @throws  PDORM_MapperException       If this mapper does not contain
     *                                      a field with the specified name
     * @access  public
     * @see     fieldName()
     * @see     fieldNames()
     */
    public function __call($method, $args)
    {
        if (preg_match('/findby(\w+)/i', $method, $match)) {
            // convert the field name
            $field_name = $this->fieldName($match[1]);

            // find the appropriate mapper for the field name
            /*
            $found = false;
            foreach ($this->fullFieldNames() as $domain_cls => $field_names) {
                if (in_array($field_name, $field_names)) {
                    $criteria = is_array($args[0]) ? $args[0] : $args; // multiple criteria
                    foreach ($criteria as $criterion) {

                    }

                    $found = true;
                    break;
                }
            }

            if (!$found) {
                $d_cls = $this->domainClass();
                throw new PDORM_MapperException("Cannot find $d_cls by field '$field_name'");
            }
            */
        }

        trigger_error(sprintf('Call to undefined function: %s::%s().',
            get_class($this), $method), E_USER_ERROR);
    }

    /**
     * Gets this mapper's database connection.
     *
     * @return  PDORM_Connection        This mapper's database connection handle
     * @access  public
     */
    public function conn()
    {
        return PDORM::getConnection($this->connection_alias);
    }

    /**
     * Gets the name of the domain class for this mapper.
     *
     * @return  string          The name of this mapper's domain class
     * @access  public
     */
    public function domainClass()
    {
        if (!$this->domain_cls) {
            $prefix = PDORM::mapperPrefix();
            $this->domain_cls = preg_replace('/^' . preg_quote($prefix) . '/', '',
                get_class($this));
        }

        return $this->domain_cls;
    }

    /**
     * Gets an array of relation data between this mapper's domain class and
     * other classes in the domain.
     *
     * @return  array           This mapper's domain class' relation data
     * @access  public
     */
    public function getRelations()
    {
        $domain = PDORM_Domain::getInstance();
        if ($domain->isSetup($this->domainClass())) {
            return $domain->getRelations($this);
        } else {
            // return raw relationship data...should only happen in __construct
            return array(
                'domain_class'  => $this->domainClass(),
                'has_one'       => $this->has_one,
                'has_many'      => $this->has_many,
                'belongs_to'    => $this->belongs_to,
                'many_to_many'  => $this->many_to_many
            );
        }
    }

    /**
     * Asserts this mapper's domain class has a defined relation with the given
     * domain class.
     *
     * @param   string  $rel        The name of the related object class
     * @return  void
     * @throws  PDORM_RelationException     If the relation has not been
     *                                      defined
     * @access  public
     */
    public function assertRelation($rel)
    {
        $relations = $this->getRelations();
        if (!isset($relations[$rel])) {
            throw new PDORM_RelationException($this->domainClass(), $rel);
        }
    }

    /**
     * Gets the primary key to use for this mapper.
     *
     * @return  string      The primary key for this mapper
     * @access  public
     */
    public function primaryKey()
    {
        return $this->primary_key;
    }

    /**
     * Gets the table name for this mapper.
     *
     * @return  string      The table name for this mapper
     * @access  public
     */
    public function tableName()
    {
        if (!$this->table_name) {
            $this->table_name = PDORM_Inflector::tableName($this->domainClass());
        }

        return $this->table_name;
    }

    /**
     * Returns the sequence name to use for this mapper when generating id's.
     *
     * @return  string      The name of the sequence to use for this mapper
     * @access  public
     */
    public function sequenceName()
    {
        if (!$this->sequence_name) {
            $seqname_format = $this->conn()->getAttribute(PDODB::ATTR_SEQNAME_FORMAT);
            $this->sequence_name = sprintf($seqname_format, $this->tableName());
        }

        return $this->sequence_name;
    }

    /**
     * Returns an array of the field names of this mapper's domain class.
     *
     * @return  array               An array of this mapper's field names
     * @access  public
     */
    public function fieldNames()
    {
        if (!$this->field_names) {
            $this->field_names = $this->conn()->listColumns($this->tableName());
        }

        return $this->field_names;
    }

    /**
     * Gets an array of all domain class names this mapper's domain class
     * is related to one of.
     *
     * @return  array               An array of this mapper's domain class'
     *                              object names
     * @access  public
     */
    public function objectNames()
    {
        if (!$this->object_names) {
            $relations = $this->getRelations();
            $this->object_names = array_merge(array_keys($relations['has_one']),
                array_keys($relations['belongs_to']));

            // include object names from parent domain classes
            if ($parent = $this->parentMapper()) {
                $this->object_names = array_merge($this->object_names,
                    $parent->objectNames());
            }
        }

        return $this->object_names;
    }

    /**
     * Gets the names of all collections contained in this mapper.
     *
     * @return  array               An array of singular => plural names of
     *                              child collections
     * @access  public
     */
    public function collectionNames()
    {
        if (!$this->collection_names) {
            $relations = $this->getRelations();
            $this->collection_names = array();
            foreach (array_keys($relations['has_many']) as $rel) {
                $this->collection_names[$rel] = PDORM_Inflector::toPlural($rel);
            }
            foreach (array_keys($relations['many_to_many']) as $rel) {
                $this->collection_names[$rel] = PDORM_Inflector::toPlural($rel);
            }

            // include collection names from parent domain classes
            if ($parent = $this->parentMapper()) {
                $this->collection_names = array_merge($this->collection_names,
                    $parent->collectionNames());
            }
        }

        return $this->collection_names;
    }

    /**
     * Determines if this mapper directly extends PDORM_Mapper.
     *
     * @return  bool            True if this mapper is a direct descendant
     * @access  public
     */
    public function isBaseMapper()
    {
        return get_parent_class($this) == __CLASS__;
    }

    /**
     * Gets the name of the base mapper class that extends PDORM_Mapper.
     *
     * @return  string          The name of the base mapper class
     * @access  public
     */
    public function baseMapper()
    {
        return $this->isBaseMapper() ? get_class($this) : $this->callParent(__FUNCTION__);
    }

    /**
     * Gets the instance of this mapper's parent mapper class. Returns NULL if
     * this mapper is a {@link isBaseMapper() base mapper}.
     *
     * @return  PDORM_Mapper     This mapper's parent class instance
     * @access  public
     */
    public function parentMapper()
    {
        if ($this->isBaseMapper()) {
            return null;
        }

        $p_cls = get_parent_class($this);
        return call_user_func(array($p_cls, 'getInstance'));
    }

    /**
     * Calls a method of this mapper's parent mapper, should one exist.
     *
     * @param   string  $method     The name of the method to call
     * @param   array   $args       An array of arguments to pass to the method
     * @return  mixed               The result of the function call
     * @access  public
     */
    public function callParent($method, $args = array())
    {
        if ($parent = $this->parentMapper()) {
            return call_user_func_array(array($parent, $method), $args);
        }
    }

    /**
     * Creates an instance of this mapper's object class. Checks the cache
     * first to make sure the same objects are recycled.
     *
     * @param   array   $attributes         The attributes for this object
     * @param   bool    $already_in_db      True if this object is already in
     *                                      the database
     * @return  object                      The newly created object
     * @access  public
     */
    public function create($attributes = array(), $already_in_db = false)
    {
        $p_key = $this->primaryKey();

        if (isset($attributes[$p_key])) {
            // is the object a child?
            if (isset($this->inheritance_col)
                && isset($attributes[$this->inheritance_col])) {
                    // create a child object
                    $child_mapper = PDORM::mapperFactory($attributes[$this->inheritance_col]);
                    $child = $child_mapper->find($attributes[$p_key]);
                    foreach ($attributes as $attr => $value) {
                        $child->set($attr, $value);
                    }
                    $child->markClean();
                    return $child;
            }

            // has the object already been fetched?
            if ($obj = PDORM::get($this->domainClass(), $attributes[$p_key])) {
                return $obj;
            }
        }

        $obj_cls = $this->domainClass();
        return new $obj_cls($attributes, $already_in_db);
    }

    /**
     * Finds a record in the database. Implicitly throws PDODB_Connection_Exception
     * on failure. May be called several different ways:
     *
     * <code>
     * find(1)
     * </code>
     *
     * Select the record with primary key 1 and return the object.
     *
     * <code>
     * find(array(1))
     * </code>
     *
     * Select the record with primary key 1 and return the object as the first
     * element of a collection.
     *
     * <code>
     * find(array(1, 2))
     * </code>
     *
     * Select the records with primary keys 1 and 2 and return the objects in
     * a collection.
     *
     * <code>
     * find(1, array('select' => array('first_name', 'last_name')))
     * </code>
     *
     * Select only the first_name and last_name fields from the record with
     * primary key 1 and return the object.
     *
     * <code>
     * find(array('where' => 'balance = 0'))
     * </code>
     *
     * Select all records where the balance = 0 and return the objects in a
     * collection.
     *
     * <code>
     * find(array('where' => 'balance > 0', 'limit' => 1))
     * </code>
     *
     * Select the first record with 'balance' > 0 and return the object.
     *
     * @return  mixed                           Either a domain object
     *                                          collection, an object, or NULL
     *                                          if nothing was found
     * @throws  Exception                       If the find options array
     *                                          contains invalid keys
     * @throws  PDODB_Connection_Exception      If the query fails
     * @access  public
     */
    public function find()
    {
        $args = func_get_args();

        // get and validate find options
        $last_arg = $args[count($args) - 1];
        $options = is_array($last_arg) && (PDORM_is_assoc($last_arg) || empty($last_arg))
            ? array_pop($args)
            : array();
        PDORM_assert_valid_keys($options, self::$valid_find_keys);

        // return a collection (is the LIMIT 1)?
        $return_coll = !isset($options['limit']) || $options['limit'] != 1;

        // create the SELECT query
        $conn = $this->conn();
        $select = isset($options['select']) ? $options['select'] : '*';
        $query = $conn->select($select, $this->tableName());

        // DISTINCT
        if (isset($options['distinct'])) {
            $query->distinct();
        }

        // JOIN
        if (!empty($options['join'])) {
            $query->join($options['join']);
        }

        // WHERE - are there id's to find?
        if ($count = count($args)) {
            if (is_array($args[0])) {
                // id(s) in an array, return collection
                $args = array_values($args[0]);
            } elseif ($count == 1) {
                // don't return collection, only one id
                $return_coll = false;
            }
            $p_key = $this->primaryKey();
            foreach ($args as $id) {
                $query->where($p_key, $id, 'OR');
            }
        }

        // other WHERE conditions
        if (isset($options['where'])) {
            $where = $options['where'];
            if (!is_array($where)) {
                $where = array($where);
            }
            $query->where($where, 'AND');
        }

        // GROUP BY
        if (!empty($options['group_by'])) {
            $query->groupBy($options['group_by']);
        }

        // HAVING
        if (!empty($options['having'])) {
            $query->having($options['having']);
        }

        // ORDER BY
        if (isset($options['order_by'])) {
            $order_by = $options['order_by'];
            if (!is_array($order_by)) {
                $order_by = array($order_by, 'ASC');
            }
            $query->orderBy($order_by[0], $order_by[1]);
        }

        // LIMIT and OFFSET
        if (!empty($options['limit'])) {
            if (isset($options['offset'])) {
                $query->limit($options['limit'], $options['offset']);
            } else {
                $query->limit($options['limit']);
            }
        }

        // execute the query
        $stmt = $query->execute();

        // if no records found, return null
        $results = $stmt->fetchRows();
        $stmt->free();
        if (count($results) == 0) {
            return null;
        }

        // create objects
        foreach ($results as $key => $attributes) {
            $results[$key] = $this->create($attributes, true);
        }

        // return a single object
        if ($return_coll == false) {
            return $results[0];
        }

        // return a collection of objects
        $collection = new PDORM_Collection($this->domainClass());
        foreach ($results as $obj) {
            $collection[] = $obj;
        }
        return $collection;
    }

    /**
     * Saves an object to the database. Automatically serializes child arrays
     * and objects and saves dependent objects. Implicitly throws
     * PDODB_Connection_Exception on failure.
     *
     * @param   PDORM_Record    $record             The record to save
     * @param   bool            $check_children     True to save this object's
     *                                              child objects if they are
     *                                              dependent
     * @return  void
     * @throws  PDODB_Connection_Exception          If the query fails
     * @access  public
     */
    public function save(PDORM_Record &$record, $check_children = true)
    {
        if (!$record->isDirty() || $record->isTemporary()) {
            return;
        }
        $already_in_db = $record->isAlreadyInDb();
        if ($already_in_db === false) {
            if ($record->isTemporary()) {
                return;
            }
        }

        $this->setupRelations();

        $data = array();
        foreach ($this->fieldNames() as $name) {
            $data[$name] = (is_array($record->$name) || is_object($record->$name))
                ? serialize($record->$name)
                : $record->$name;
        }

        // update or insert?
        $conn = $this->conn();
        if ($record->isAlreadyInDb()) {
            $conn->update($this->tableName(), $data)->where(
                $this->primaryKey(), $record->getId())->execute();
        } else {
            $conn->insert($this->tableName(), $data)->execute();

            // update the object's id and put it in the cache?
        }

        $record->markAlreadyInDb();
        $record->markClean();

        // save parent attributes?
        if (!$this->isBaseMapper()) {
            parent::save($record, false);
        }

        // save child objects? need to make has_one, etc. private?
        if ($check_children) {
            foreach (array_keys($this->has_one) as $child_obj) {
                if (isset($record->$child_obj) && $this->dependent($child_obj)) {
                    $record->$child_obj->save();
                }
            }
            foreach (array_keys($this->has_many) as $child_coll) {
                if (isset($record->$child_coll) && $this->dependent($child_coll)) {
                    foreach ($record->$child_coll as $child_obj) {
                        $child_obj->save();
                    }
                }
            }
        }
    }

    /**
     * Deletes the specified record from the database. Implicitly throws
     * PDODB_Connection_Exception on failure.
     *
     * @param   PDORM_Record    $record         The record to delete
     * @return  void
     * @throws  PDODB_Connection_Exception      If the query fails
     * @access  public
     */
    public function delete(PDORM_Record &$record)
    {

    }

    /**
     * Determines if the specified object class is dependent upon this one.
     * Dependent objects are automatically saved when this object is saved.
     *
     * @param   string  $rel        The name of the related object class
     * @return  bool                True if the object class is dependent
     * @throws  PDORM_RelationException     If this mapper has no known relation
     *                                      to the given object class
     * @access  public
     */
    public function dependent($rel)
    {
        $rel = strtolower($rel);
        $this->assertRelation($rel);
        return isset($this->rel_options[$rel]['dependent'])
            ? $this->rel_options[$rel]['dependent']
            : null;
    }

    /**
     * Gets the foreign key to use for the specified object class.
     *
     * @param   string  $rel        The name of the related object class
     * @return  string              The foreign key to use
     * @throws  PDORM_RelationException     If this mapper has no known relation
     *                                      to the given object class
     * @access  public
     */
    public function foreignKey($rel)
    {
        $rel = strtolower($rel);
        $this->assertRelation($rel);
        return $this->rel_options[$rel]['foreign_key'];
    }

    /**
     * Gets the key to use for this object class in the join table with the
     * specified object class.
     *
     * @param   string  $rel        The name of the related object class
     * @return  string              The foreign key to use
     * @throws  PDORM_RelationException     If this mapper has no known relation
     *                                      to the given object class
     * @access  public
     */
    public function ownForeignKey($rel)
    {
        $rel = strtolower($rel);
        $this->assertRelation($rel);
        return isset($this->rel_options[$rel]['own_foreign_key'])
            ? $this->rel_options[$rel]['own_foreign_key']
            : null;
    }

    /**
     * Gets the name of the association table (join table) to use for the given
     * relation.
     *
     * @param   string  $rel        The name of the related object class
     * @return  string              The join table name to use
     * @throws  PDORM_RelationException     If this mapper has no known relation
     *                                      to the given object class
     * @access  public
     */
    public function joinTable($rel)
    {
        $rel = strtolower($rel);
        $this->assertRelation($rel);
        return isset($this->rel_options[$rel]['join_table'])
            ? $this->rel_options[$rel]['join_table']
            : null;
    }

    /**
     * Gets the finder options to use for the specified object class.
     *
     * @param   string  $rel        The name of the related object class
     * @return  array               The finder options to use
     * @throws  PDORM_RelationException     If this mapper has no known relation
     *                                      to the given object class
     * @access  public
     */
    public function findOptions($rel)
    {
        $rel = strtolower($rel);
        $this->assertRelation($rel);
        return $this->rel_options[$rel]['find'];
    }

    /**
     * Determines if this mapper object contains one of the specified object
     * class.
     *
     * @param   string  $rel        The name of the related object class
     * @return  bool                True if this mapper's object class contains
     *                              one of the specified object class
     * @access  public
     */
    public function hasOne($rel)
    {
        $relations = $this->getRelations();
        return in_array(strtolower($rel), array_keys($relations['has_one']));
    }

    /**
     * Determines if this mapper object belongs to another.
     *
     * @param   string  $rel        The name of the related object class
     * @return  bool                True if this mapper's object class is
     *                              contained by the specified object class
     * @access  public
     */
    public function belongsTo($rel)
    {
        $relations = $this->getRelations();
        return in_array(strtolower($rel), array_keys($relations['belongs_to']));
    }

    /**
     * Determines if this mapper object contains many of the specified object
     * class.
     *
     * @param   string  $rel        The name of the related object class
     * @return  bool                True if this mapper's object class contains
     *                              one of the specified object class
     * @access  public
     */
    public function hasMany($rel)
    {
        return in_array(strtolower($rel), array_keys($this->collectionNames()));
    }

}

?>
